/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.factory.server.scheduler;

import com.factory.server.bo.Edge;
import com.factory.server.bo.Fibonacci;
import com.factory.server.bo.Prime;
import com.factory.server.bo.UserInput;
import com.factory.server.cache.InputCache;
import com.factory.server.cache.OutputCache;
import com.factory.server.consumers.SchedulerTasksQueue;
import com.factory.server.dao.DAODispatcherIface;
import com.factory.server.dto.EdgeDTO;
import com.factory.server.dto.UserTaskDTO;
import com.factory.server.helper.InputWrapper;
import com.factory.server.json.Plan;
import com.factory.server.json.SchedulerResult;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

/**
 * According to chosen problem and input creadtes plans offer.
 *
 * @author milos
 */
@Service
public class SchedulerComputerImpl implements SchedulerComputerIface {

    static final Logger LOG = Logger.getLogger(SchedulerComputerImpl.class.getName());
    @Autowired
    @Qualifier("daoDispatcher")
    private DAODispatcherIface genericDAODispacher;
    private SchedulerTasksQueue schedulerTasksQueue;
    private int numberOfThreads;
    @Autowired
    private SchedulerSpanningTree schedulerSpanningTree;
    @Autowired
    private SchedulerSPT schedulerSPT;

    public SchedulerResult computeSchedulerResult(UserTaskDTO userTaskDTO, SchedulerTasksQueue schedulerTasksQueue, int numberOfThreads) {
        LOG.log(Level.INFO, "Scheduling started.");
        this.numberOfThreads = numberOfThreads;
        this.schedulerTasksQueue = schedulerTasksQueue;

        String problemId = userTaskDTO.getProblemId();

        if (problemId.equals("ERATOSTHENES")) {
            return scheduleEratoshenes(userTaskDTO);
        }

        if (problemId.equals("FIBB")) {
            return scheduleFibonacci(userTaskDTO);
        }

        if (problemId.equals("TOPOLOGICAL_ORDER")) {
            return scheduleTopological(userTaskDTO); //TODO
        }

        if (problemId.equals("LEVENSHTEIN")) {
            return scheduleDefault(userTaskDTO); //TODO
        }

        if (problemId.equals("SHORTEST_PATH")) {
            return schedulerSPT.scheduleSPT(userTaskDTO, schedulerTasksQueue, numberOfThreads);
        }

        if (problemId.equals("SPANN_TREE")) {
            return schedulerSpanningTree.scheduleSpanningTree(userTaskDTO, schedulerTasksQueue, numberOfThreads);
        }

        if (problemId.equals("SORT")) {
            return scheduleSort(userTaskDTO);
        }

        if (problemId.equals("TSP")) {
            return scheduleDefault(userTaskDTO); //TODO
        }

        return null;
    }

    public SchedulerResult scheduleEratoshenes(UserTaskDTO userTaskDTO) {

        LOG.log(Level.INFO, "Scheduling for Eratostenes started. Input id: " + userTaskDTO.getInputId());

        List<UserInput> valueAsList = null;

        InputWrapper inputFromCache = InputCache.get(userTaskDTO.getInputId());
        if (inputFromCache != null) {
            valueAsList = (List<UserInput>) (List<?>) inputFromCache.getDBRecords();
        } else {
            valueAsList = genericDAODispacher.getByProperty("inputId", userTaskDTO.getInputId(), UserInput.class);
        }

        Prime maximalPrime = null;
        if (OutputCache.primes != null && !OutputCache.primes.isEmpty()) {
            maximalPrime = OutputCache.primes.get(OutputCache.primes.size() - 1);
        }
        if (maximalPrime == null) {
            maximalPrime = new Prime((int) 0);
        }
        ArrayList<Plan> plans = new ArrayList<Plan>();

        String value = valueAsList.get(0).getValue();
        Long inputValue = Long.parseLong(value);


        if (maximalPrime.getValue() >= inputValue) { //najvacsie prvocislo v cache je vacsie ako vstupna hodnota?
            LOG.log(Level.INFO, "Result is in DB. No computation needed.");
            return createResult(0, userTaskDTO);

        }
        LOG.log(Level.INFO, "Result is not in DB. Going to compute...");
        ;
        long computationTime = (long) (0.000000000001 * Math.log10(Math.log10(inputValue)) * inputValue * inputValue);

        return createResult(computationTime, userTaskDTO);
    }

    private SchedulerResult scheduleSort(UserTaskDTO userTaskDTO) {

        LOG.log(Level.INFO, "Scheduling for Sort started. Input id: " + userTaskDTO.getInputId());
        List<UserInput> dataList = genericDAODispacher.getByProperty("inputId", userTaskDTO.getInputId(), UserInput.class);
        long computationTime = (long) (Math.pow(dataList.size(), 2) * 0.0000001);  //constant is 0.0000007

        return createResult(computationTime, userTaskDTO);
    }

    private SchedulerResult scheduleTopological(UserTaskDTO userTaskDTO) {

        LOG.log(Level.INFO, "Scheduling for Sort started. Input id: " + userTaskDTO.getInputId());
        List<Edge> edgeList = null;
        InputWrapper inputFromCache = InputCache.get(userTaskDTO.getInputId());
        if (inputFromCache != null) {
            edgeList = (List<Edge>) (List<?>) inputFromCache.getDBRecords();
        } else {
            edgeList = genericDAODispacher.getByProperty("inputId", userTaskDTO.getInputId(), Edge.class);
        }
        Set<Integer> nodes = new HashSet<Integer>();
        List<EdgeDTO> edgeDTOList = new ArrayList<EdgeDTO>(edgeList.size());
        for (Edge edge : edgeList) {
            nodes.add(edge.getSource());
            nodes.add(edge.getTarget());
            edgeDTOList.add(new EdgeDTO(edge.getSource(), edge.getTarget(), edge.getCost()));
        }
        long computationTime = (long) (nodes.size() * edgeDTOList.size() * 0.0001);  //constant is 0.0000007

        return createResult(computationTime, userTaskDTO);
    }

    private SchedulerResult scheduleFibonacci(UserTaskDTO userTaskDTO) {
        if (OutputCache.cacheMap != null && OutputCache.cacheMap.containsKey(userTaskDTO.getInputId())) {
            return createResult(0, userTaskDTO);
        }
        UserInput input = null;
        if (InputCache.cacheMap != null && InputCache.cacheMap.containsKey(userTaskDTO.getInputId())) {
            input = (UserInput) InputCache.get(userTaskDTO.getInputId()).getDBRecords().get(0);
        } else {
            input = genericDAODispacher.getByProperty("inputId", userTaskDTO.getInputId(), UserInput.class).get(0);
        }
        long inputNumber = Long.valueOf(input.getValue());
//        List<Fibonacci> alreadyCountedList = genericDAODispacher.getByProperty("index", BigInteger.valueOf(inputNumber), Fibonacci.class);
        Boolean alreadyCOmputed = genericDAODispacher.contains("index", BigInteger.valueOf(inputNumber), Fibonacci.class);
        if (alreadyCOmputed != null && alreadyCOmputed) {
            return createResult(0, userTaskDTO);
        }


        double resultLength = inputNumber * Math.log10(1.618) - Math.log10(Math.sqrt(5));
        LOG.info("Result Fibonacci number length: " + resultLength);

        long computationTime = (long) (0.0000001 * resultLength * inputNumber);
        return createResult(computationTime, userTaskDTO);
    }

    private SchedulerResult scheduleDefault(UserTaskDTO userTaskDTO) {
        ArrayList<Plan> plans = new ArrayList<Plan>();
        plans.add(new Plan(userTaskDTO.getUserId(), userTaskDTO.getProblemId(), "immediately", 4, userTaskDTO.getInputId()));

        SchedulerResult schedulerResult = new SchedulerResult(userTaskDTO.getUserId(), userTaskDTO.getProblemId(), plans);
        return createResult(0, userTaskDTO);
    }

    private SchedulerResult createResult(long computationTime, UserTaskDTO userTaskDTO) {
        ArrayList<Plan> plans = new ArrayList<Plan>();
        //going to compute offsets
        long offsetForPriorityPlan = getOffsetPriorityPlan(computationTime);
        long offset = getOffset(computationTime);

        long plannedFinishTime = computationTime + offset;
        long plannedPriorityFinishTime = (computationTime) + (offsetForPriorityPlan);
        long plannedCloudFinishTime = computationTime;

        String noPriorityTime = null;
        //kontrola ci nepresvihne deadline 1
        if (plannedFinishTime > TimeUnit.HOURS.toMillis(12)) {
            noPriorityTime = "not guaranteed in 12 hours";
        } else {
            noPriorityTime = "in 12 hours";
        }

        String timeAsDateForPriorityPlan = getStringTime(plannedPriorityFinishTime);
        String timeAsDateForCloudPlan = getStringTime(computationTime);

        //no priority plan
        Plan noPriorityPlan = new Plan(userTaskDTO.getUserId(), userTaskDTO.getInputId(), userTaskDTO.getProblemId(), 4, noPriorityTime, plannedFinishTime, computationTime);
        noPriorityPlan.setDeadline(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(12)); //in12 hours
        plans.add(noPriorityPlan);

        //priority plan
        plans.add(new Plan(userTaskDTO.getUserId(), userTaskDTO.getInputId(), userTaskDTO.getProblemId(), 8, "in " + timeAsDateForPriorityPlan, plannedPriorityFinishTime, computationTime));

        //cloud plan
        Plan cloudPlan = new Plan(userTaskDTO.getUserId(), userTaskDTO.getInputId(), userTaskDTO.getProblemId(), 12, "in " + timeAsDateForCloudPlan, plannedCloudFinishTime, computationTime);
        cloudPlan.setSupported(false);
        plans.add(cloudPlan);

        SchedulerResult schedulerResult = new SchedulerResult(userTaskDTO.getUserId(), userTaskDTO.getProblemId(), plans);
        return schedulerResult;

    }

    private String getStringTime(long millisTime) {

        String timeAsDateForPriorityPlan = String.format("%d hours, %d min, %d sec",
                TimeUnit.MILLISECONDS.toHours(millisTime),
                TimeUnit.MILLISECONDS.toMinutes(millisTime)
                - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(millisTime)),
                TimeUnit.MILLISECONDS.toSeconds(millisTime)
                - TimeUnit.HOURS.toSeconds(TimeUnit.MILLISECONDS.toHours(millisTime))
                - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millisTime)
                - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(millisTime))));

        return timeAsDateForPriorityPlan;

    }

    private long getOffsetPriorityPlan(long computationTime) {
        MachineList machineList = new MachineList(numberOfThreads);
//        LOG.log(Level.INFO, "Size of schedulerTasksQueue: " + schedulerTasksQueue.getPlans().size());
        Plan priorityPlan = new Plan();
        priorityPlan.setComputationTime(computationTime);
        priorityPlan.setDeadline(0);
        int offsetForPriorityPlan = machineList.getOffset(schedulerTasksQueue, priorityPlan);
        return offsetForPriorityPlan;
    }

    private long getOffset(long computationTime) {
        MachineList machineList = new MachineList(numberOfThreads);
        long in12Hours = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(12);
        Plan freePlan = new Plan();
        freePlan.setComputationTime(computationTime);
        freePlan.setDeadline(in12Hours);
        int offset = machineList.getOffset(schedulerTasksQueue, freePlan);
        return offset;
    }
}
